msg_tool\scripts\hexen_haus\archive/
odio.rs1use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use anyhow::{Result, anyhow};
6use std::io::{Read, Seek, SeekFrom};
7use std::sync::{Arc, Mutex};
8
9const ODIO_SIGNATURE: &[u8; 4] = b"ODIO";
10const HEADER_CHECK_OFFSET: u64 = 0x0A;
11const HEADER_CHECK_VALUE: u32 = 0xCCAE_01FF;
12const INDEX_START: u64 = 0x12;
13const INDEX_ENTRY_SIZE: u64 = 6;
14const ENTRY_HEADER_SIZE: u64 = 0x2C;
15
16#[derive(Debug)]
17pub struct HexenHausOdioArchiveBuilder;
19
20impl HexenHausOdioArchiveBuilder {
21 pub const fn new() -> Self {
23 HexenHausOdioArchiveBuilder
24 }
25}
26
27impl ScriptBuilder for HexenHausOdioArchiveBuilder {
28 fn default_encoding(&self) -> Encoding {
29 Encoding::Cp932
30 }
31
32 fn default_archive_encoding(&self) -> Option<Encoding> {
33 Some(Encoding::Cp932)
34 }
35
36 fn build_script(
37 &self,
38 buf: Vec<u8>,
39 _filename: &str,
40 _encoding: Encoding,
41 archive_encoding: Encoding,
42 config: &ExtraConfig,
43 _archive: Option<&Box<dyn Script>>,
44 ) -> Result<Box<dyn Script + Send + Sync>> {
45 Ok(Box::new(HexenHausOdioArchive::new(
46 MemReader::new(buf),
47 archive_encoding,
48 config,
49 )?))
50 }
51
52 fn build_script_from_file(
53 &self,
54 filename: &str,
55 _encoding: Encoding,
56 archive_encoding: Encoding,
57 config: &ExtraConfig,
58 _archive: Option<&Box<dyn Script>>,
59 ) -> Result<Box<dyn Script + Send + Sync>> {
60 if filename == "-" {
61 let data = crate::utils::files::read_file(filename)?;
62 return Ok(Box::new(HexenHausOdioArchive::new(
63 MemReader::new(data),
64 archive_encoding,
65 config,
66 )?));
67 }
68 let file = std::fs::File::open(filename)?;
69 let reader = std::io::BufReader::new(file);
70 Ok(Box::new(HexenHausOdioArchive::new(
71 reader,
72 archive_encoding,
73 config,
74 )?))
75 }
76
77 fn build_script_from_reader<'a>(
78 &self,
79 reader: Box<dyn ReadSeek + Send + Sync + 'a>,
80 _filename: &str,
81 _encoding: Encoding,
82 archive_encoding: Encoding,
83 config: &ExtraConfig,
84 _archive: Option<&Box<dyn Script>>,
85 ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
86 Ok(Box::new(HexenHausOdioArchive::new(
87 reader,
88 archive_encoding,
89 config,
90 )?))
91 }
92
93 fn extensions(&self) -> &'static [&'static str] {
94 &["bin"]
95 }
96
97 fn script_type(&self) -> &'static ScriptType {
98 &ScriptType::HexenHausOdio
99 }
100
101 fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
102 if buf_len >= ODIO_SIGNATURE.len() && buf.starts_with(ODIO_SIGNATURE) {
103 Some(10)
104 } else {
105 None
106 }
107 }
108
109 fn is_archive(&self) -> bool {
110 true
111 }
112}
113
114#[derive(Debug, Clone)]
115struct HexenHausOdioEntry {
116 name: String,
117 offset: u64,
118 size: u64,
119}
120
121#[derive(Debug)]
122pub struct HexenHausOdioArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
124 reader: Arc<Mutex<T>>,
125 entries: Vec<HexenHausOdioEntry>,
126 _mark: std::marker::PhantomData<&'b ()>,
127}
128
129impl<'b, T: Read + Seek + std::fmt::Debug + 'b> HexenHausOdioArchive<'b, T> {
130 pub fn new(mut reader: T, _archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
132 reader.seek(SeekFrom::Start(0))?;
133 let mut signature = [0u8; 4];
134 reader.read_exact(&mut signature)?;
135 if signature != *ODIO_SIGNATURE {
136 return Err(anyhow!("Invalid HexenHaus ODIO signature"));
137 }
138
139 let reserved = reader.read_u32()?;
140 if reserved != 0 {
141 return Err(anyhow!("Unexpected reserved field in ODIO header"));
142 }
143
144 reader.seek(SeekFrom::Start(HEADER_CHECK_OFFSET))?;
145 let header_check = reader.read_u32()?;
146 if header_check != HEADER_CHECK_VALUE {
147 return Err(anyhow!("Invalid HexenHaus ODIO header check value"));
148 }
149
150 let file_length = reader.seek(SeekFrom::End(0))?;
151 reader.seek(SeekFrom::Start(INDEX_START))?;
152 let first_offset = u64::from(reader.read_u32()?);
153 if first_offset < INDEX_START {
154 return Err(anyhow!("First entry offset precedes index start"));
155 }
156 if first_offset > file_length {
157 return Err(anyhow!("First entry offset exceeds file length"));
158 }
159
160 let index_len = first_offset
161 .checked_sub(INDEX_START)
162 .ok_or_else(|| anyhow!("Invalid index length in ODIO archive"))?;
163 if index_len % INDEX_ENTRY_SIZE != 0 {
164 return Err(anyhow!("ODIO index length is not aligned"));
165 }
166 let entry_count_u64 = index_len / INDEX_ENTRY_SIZE;
167 let entry_count =
168 usize::try_from(entry_count_u64).map_err(|_| anyhow!("ODIO entry count overflow"))?;
169 if entry_count == 0 {
170 return Err(anyhow!("ODIO archive contains no entries"));
171 }
172
173 let mut entries = Vec::with_capacity(entry_count);
174 let mut index_offset = INDEX_START;
175 let mut next_offset = first_offset;
176
177 for i in 0..entry_count {
178 let entry_offset = next_offset;
179
180 index_offset = index_offset
181 .checked_add(INDEX_ENTRY_SIZE)
182 .ok_or_else(|| anyhow!("Index offset overflow"))?;
183
184 if i + 1 == entry_count {
185 next_offset = file_length;
186 } else {
187 if index_offset + 4 > file_length {
188 return Err(anyhow!("Index offset exceeds file length"));
189 }
190 reader.seek(SeekFrom::Start(index_offset))?;
191 next_offset = u64::from(reader.read_u32()?);
192 }
193
194 if entry_offset > file_length {
195 return Err(anyhow!("Entry offset exceeds file length"));
196 }
197 if next_offset > file_length {
198 return Err(anyhow!("Entry extends beyond file length"));
199 }
200 if next_offset < entry_offset {
201 return Err(anyhow!("Entry offsets are out of order"));
202 }
203
204 let size = next_offset - entry_offset;
205 if size == 0 {
206 continue;
207 }
208
209 let name = format!("{:04}.ogg", i);
210 entries.push(HexenHausOdioEntry {
211 name,
212 offset: entry_offset,
213 size,
214 });
215 }
216
217 if entries.is_empty() {
218 return Err(anyhow!("ODIO archive contains no readable entries"));
219 }
220
221 reader.seek(SeekFrom::Start(0))?;
222 Ok(HexenHausOdioArchive {
223 reader: Arc::new(Mutex::new(reader)),
224 entries,
225 _mark: std::marker::PhantomData,
226 })
227 }
228}
229
230impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script
231 for HexenHausOdioArchive<'b, T>
232{
233 fn default_output_script_type(&self) -> OutputScriptType {
234 OutputScriptType::Json
235 }
236
237 fn default_format_type(&self) -> FormatOptions {
238 FormatOptions::None
239 }
240
241 fn is_archive(&self) -> bool {
242 true
243 }
244
245 fn iter_archive_filename<'a>(
246 &'a self,
247 ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
248 Ok(Box::new(
249 self.entries.iter().map(|entry| Ok(entry.name.clone())),
250 ))
251 }
252
253 fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
254 Ok(Box::new(self.entries.iter().map(|entry| Ok(entry.offset))))
255 }
256
257 fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
258 if index >= self.entries.len() {
259 return Err(anyhow!(
260 "Index out of bounds: {} (total files: {})",
261 index,
262 self.entries.len()
263 ));
264 }
265 let entry = self.entries[index].clone();
266
267 let decrypt = if entry.size >= ENTRY_HEADER_SIZE {
268 let mut header = [0u8; 4];
269 let mut guard = self
270 .reader
271 .lock()
272 .map_err(|e| anyhow!("Failed to lock reader: {}", e))?;
273 guard.seek(SeekFrom::Start(entry.offset))?;
274 guard.read_exact(&mut header)?;
275 header == *b"ONCE"
276 } else {
277 false
278 };
279
280 let (data_offset, data_size) = if decrypt {
281 let data_offset = entry
282 .offset
283 .checked_add(ENTRY_HEADER_SIZE)
284 .ok_or_else(|| anyhow!("Entry data offset overflow"))?;
285 let data_size = entry
286 .size
287 .checked_sub(ENTRY_HEADER_SIZE)
288 .ok_or_else(|| anyhow!("Entry data size underflow"))?;
289 (data_offset, data_size)
290 } else {
291 (entry.offset, entry.size)
292 };
293
294 Ok(Box::new(OdioEntry {
295 name: entry.name,
296 reader: self.reader.clone(),
297 data_offset,
298 data_size,
299 pos: 0,
300 decrypt,
301 }))
302 }
303}
304
305#[derive(Debug)]
306struct OdioEntry<T: Read + Seek> {
307 name: String,
308 reader: Arc<Mutex<T>>,
309 data_offset: u64,
310 data_size: u64,
311 pos: u64,
312 decrypt: bool,
313}
314
315impl<T: Read + Seek + std::fmt::Debug + Send + Sync> ArchiveContent for OdioEntry<T> {
316 fn name(&self) -> &str {
317 &self.name
318 }
319
320 fn size(&self) -> Option<u64> {
321 Some(self.data_size)
322 }
323
324 fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
325 Ok(Box::new(self))
326 }
327}
328
329impl<T: Read + Seek> Read for OdioEntry<T> {
330 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
331 let total_size = self.data_size;
332 if self.pos >= total_size {
333 return Ok(0);
334 }
335
336 let remaining = total_size - self.pos;
337 let remaining_usize = match usize::try_from(remaining) {
338 Ok(value) => value,
339 Err(_) => usize::MAX,
340 };
341 let to_read = remaining_usize.min(buf.len());
342 if to_read == 0 {
343 return Ok(0);
344 }
345
346 let absolute_offset = match self.data_offset.checked_add(self.pos) {
347 Some(offset) => offset,
348 None => {
349 return Err(std::io::Error::new(
350 std::io::ErrorKind::InvalidInput,
351 "Read position overflow",
352 ));
353 }
354 };
355
356 let mut guard = self.reader.lock().map_err(|e| {
357 std::io::Error::new(
358 std::io::ErrorKind::Other,
359 format!("Failed to lock mutex: {}", e),
360 )
361 })?;
362 guard.seek(SeekFrom::Start(absolute_offset))?;
363 let bytes_read = guard.read(&mut buf[..to_read])?;
364 drop(guard);
365
366 if self.decrypt {
367 for byte in &mut buf[..bytes_read] {
368 *byte = byte.rotate_right(4);
369 }
370 }
371
372 self.pos = self.pos.saturating_add(bytes_read as u64);
373 Ok(bytes_read)
374 }
375}
376
377impl<T: Read + Seek> Seek for OdioEntry<T> {
378 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
379 let new_pos = match pos {
380 SeekFrom::Start(offset) => offset,
381 SeekFrom::End(offset) => {
382 let size = i64::try_from(self.data_size).map_err(|_| {
383 std::io::Error::new(
384 std::io::ErrorKind::InvalidInput,
385 "Data size exceeds seek range",
386 )
387 })?;
388 let target = size.checked_add(offset).ok_or_else(|| {
389 std::io::Error::new(
390 std::io::ErrorKind::InvalidInput,
391 "Seek from end caused overflow",
392 )
393 })?;
394 if target < 0 {
395 return Err(std::io::Error::new(
396 std::io::ErrorKind::InvalidInput,
397 "Seek from end before start",
398 ));
399 }
400 target as u64
401 }
402 SeekFrom::Current(offset) => {
403 let current = i64::try_from(self.pos).map_err(|_| {
404 std::io::Error::new(
405 std::io::ErrorKind::InvalidInput,
406 "Current position overflow",
407 )
408 })?;
409 let target = current.checked_add(offset).ok_or_else(|| {
410 std::io::Error::new(
411 std::io::ErrorKind::InvalidInput,
412 "Seek from current caused overflow",
413 )
414 })?;
415 if target < 0 {
416 return Err(std::io::Error::new(
417 std::io::ErrorKind::InvalidInput,
418 "Seek before start",
419 ));
420 }
421 target as u64
422 }
423 };
424 self.pos = new_pos;
425 Ok(self.pos)
426 }
427
428 fn stream_position(&mut self) -> std::io::Result<u64> {
429 Ok(self.pos)
430 }
431}